其他
深入理解Babel - 项目管理工具lerna解析|得物技术
目录
一、背景
1. 工程管理
2. 代码风格
3. 文档
4. 质量管理
二、monorepo
1. 什么是monorepo
2. monorepo的优缺点
2.1 优点
2.2 缺点
2.3 选择
三、lerna
1. lerna命令集
1.1 命令行列表
1.2 全局配置项
1.3 过滤器参数
2. lerna原理解析
2.1 文件结构
2.2 命令行注册
2.3 依赖管理
2.4 lerna中涉及的git命令
四、总结
一
背景
工程管理
模块间如何方便地互相关联进行本地开发; 整个项目的版本控制; 操作自动化。
代码风格
文档
质量控制
二
monorepo
什么是monorepo
├── packages
| ├── pkg1
| | ├── package.json
| ├── pkg2
| | ├── package.json
├── package.json
├─ lerna.json
├─ package.json
└─ packages/ # 这里将存放所有子项目目录
├─ README.md
├─ babel-cli
├─ babel-code-frame
├─ babel-compat-data
├─ babel-core
├─ babel-generator
├─ babel-helper-annotate-as-pure
├─ babel-helper-builder-binary-assignment-operator-visitor
├─ babel-helper-builder-react-jsx
├─ ...
├─ rollup
├─ package.json
├─ ...
├─ plugins
├─ package.json
├─ ...
├─ awesome
├─ package.json
├─ ...
├─ rollup-starter-lib
├─ package.json
├─ ...
├─ rollup-plugin-babel
├─ package.json
├─ ...
├─ rollup-plugin-commonjs
├─ package.json
├─ ...
monorepo的优缺点
优点
便捷的代码复用与依赖管理 当所有项目代码都在一个工程里,抽离可复用的代码就十分容易了。并且抽离后,如果复用的代码有改动,可以通过一些工具,快速定位受影响的子工程,进而做到子工程的版本控制。 便捷的代码重构 通过一些工具,monorepo项目中的代码改动可以快速地定位出代码变更的影响范围,对整个工程进行快速的整体测试。而如果子工程分散在不同的工程分支里的话,通用代码的重构将难以触达各个子工程。 倡导开放、共享 monorepo项目中,开发者可以方便地看到所有子工程,这样响应了"开放、共享"的组织文化。可以激发开发者对工程质量等维护的热情(毕竟别人看不到自己的代码,乱不乱就看自己心情了),有助于团队建立良好的技术氛围。
缺点
复杂的权限管理 因为所有子工程集中在一个工程里,某些子工程如果不希望对外展示的话,monorepo的权限管理就比较难以实现了,难以锁定目标工程做独立的代码权限管理。 较高的熟悉成本 相对于multirepo,monorepo涉及各种子工程、通用依赖等,新的开发者在理解整个项目时,可能需要了解较多的信息才能入手,如通用依赖代码、各子工程功能。 较大的工程体积 很明显,所有子工程集成在一个工程里,代码体积会非常大,对文件存储系统等提出了较高的要求。 较高的质量风险 成也萧何败萧何,monorepo提供了便捷的代码复用能力,同时,一个公用模块的某个版本有bug的话,很容易影响所有用到它的子工程。此时,做好高覆盖率的单元测试就比较重要了。
选择
三
lerna
项目管理 lerna提供了一系列命令用于monorepo项目初始化、添加子项目、查看项目信息等。 依赖管理 lerna支持为monorepo项目统一管理公共依赖、自动安装各个子项目的依赖、自动创建子模块符号链接等。 版本管理 lerna可以根据项目代码的变动情况,发现影响的子项目范围,在发布时提供语义化版本推荐等,极大提升了monorepo项目的版本管理效率。
lerna命令集
命令行列表
全局配置项
lerna publish --concurrency 1
lerna bootstrap --reject-cycles
过滤器参数
lerna exec --scope my-component -- ls -la
lerna run --scope toolbar-* test
lerna run --scope package-1 --scope *-2 lint
lerna exec --ignore package-{1,2,5} -- ls -la
lerna run --ignore package-1 test
lerna run --ignore package-@(1|2) --ignore package-3 lint
# 列出自最新标记以来发生变化的包的内容
$ lerna exec --since -- ls -la
# 为自“master”以来所有发生更改的包运行测试
$ lerna run test --since master
# 列出自“某个分支”以来发生变化的所有包
$ lerna ls --since some-branch
lerna bootstrap --scope my-component --include-dependencies
# my-component 及其所有依赖项将被引导
lerna bootstrap --scope "package-*" --ignore "package-util-*" --include-dependencies
# 所有匹配 "package-util-*" 的包将被忽略,除非它们依赖于名称匹配 "package-*" 的包
lerna exec --since --include-merged-tags -- ls -la
lerna原理解析
文件结构
lerna
├─ CHANGELOG.md -- 更新日志
├─ README.md -- 文档
├─ commands -- 核心子模块
├─ core -- 核心子模块
├─ utils -- 核心子模块
├─ lerna.json -- lerna 配置文件
├─ package-lock.json -- 依赖声明
├─ package.json -- 依赖声明
├─ scripts -- 内置脚本
└─ yarn.lock -- 依赖声明
const cli = require("@lerna/cli");
const addCmd = require("@lerna/add/command");
const bootstrapCmd = require("@lerna/bootstrap/command");
const changedCmd = require("@lerna/changed/command");
const cleanCmd = require("@lerna/clean/command");
const createCmd = require("@lerna/create/command");
const diffCmd = require("@lerna/diff/command");
const execCmd = require("@lerna/exec/command");
const importCmd = require("@lerna/import/command");
const infoCmd = require("@lerna/info/command");
const initCmd = require("@lerna/init/command");
const linkCmd = require("@lerna/link/command");
const listCmd = require("@lerna/list/command");
const publishCmd = require("@lerna/publish/command");
const runCmd = require("@lerna/run/command");
const versionCmd = require("@lerna/version/command");
命令行注册
"bin": {
"lerna": "cli.js"
}
#!/usr/bin/env node
"use strict";
/* eslint-disable import/no-dynamic-require, global-require */
const importLocal = require("import-local");
if (importLocal(__filename)) {
require("npmlog").info("cli", "using local version of lerna");
} else {
require(".")(process.argv.slice(2));
}
"use strict";
const cli = require("@lerna/cli");
const addCmd = require("@lerna/add/command");
const bootstrapCmd = require("@lerna/bootstrap/command");
const changedCmd = require("@lerna/changed/command");
const cleanCmd = require("@lerna/clean/command");
const createCmd = require("@lerna/create/command");
const diffCmd = require("@lerna/diff/command");
const execCmd = require("@lerna/exec/command");
const importCmd = require("@lerna/import/command");
const infoCmd = require("@lerna/info/command");
const initCmd = require("@lerna/init/command");
const linkCmd = require("@lerna/link/command");
const listCmd = require("@lerna/list/command");
const publishCmd = require("@lerna/publish/command");
const runCmd = require("@lerna/run/command");
const versionCmd = require("@lerna/version/command");
const pkg = require("./package.json");
module.exports = main;
function main(argv) {
const context = {
lernaVersion: pkg.version,
};
return cli()
.command(addCmd)
.command(bootstrapCmd)
.command(changedCmd)
.command(cleanCmd)
.command(createCmd)
.command(diffCmd)
.command(execCmd)
.command(importCmd)
.command(infoCmd)
.command(initCmd)
.command(linkCmd)
.command(listCmd)
.command(publishCmd)
.command(runCmd)
.command(versionCmd)
.parse(argv, context);
}
const { Command } = require("@lerna/command");
class InitCommand extends Command {
...
}
class InitCommand extends Command {
...
initialize() {
throw new ValidationError(this.name, "initialize() needs to be implemented.");
}
execute() {
throw new ValidationError(this.name, "execute() needs to be implemented.");
}
}
#!/usr/bin/env node
"use strict";
/* eslint-disable import/no-dynamic-require, global-require */
const importLocal = require("import-local");
if (importLocal(__filename)) {
require("npmlog").info("cli", "using local version of lerna");
} else {
require(".")(process.argv.slice(2));
}
'use strict';
const path = require('path');
const resolveCwd = require('resolve-cwd');
const pkgDir = require('pkg-dir');
module.exports = filename => {
const globalDir = pkgDir.sync(path.dirname(filename));
const relativePath = path.relative(globalDir, filename);
const pkg = require(path.join(globalDir, 'package.json'));
const localFile = resolveCwd.silent(path.join(pkg.name, relativePath));
const localNodeModules = path.join(process.cwd(), 'node_modules');
const filenameInLocalNodeModules = !path.relative(localNodeModules, filename).startsWith('..');
// Use path.relative() to detect local package installation,
// because __filename's case is inconsistent on Windows
// Can use === when targeting Node.js 8
// See https://github.com/nodejs/node/issues/6624
return !filenameInLocalNodeModules && localFile && path.relative(localFile, filename) !== '' && require(localFile);
};
依赖管理
{
"npmClient": "yarn",
"useWorkspaces": true,
}
{
"workspaces": [
"packages/*"
]
}
{
"workspaces": {
"packages": [
"Packages/*",
],
"nohoist": [
"**"
]
}
}
lerna中涉及的git命令
git init
git rev-parse
git describe
git rev-list
git tag
git log
git config
git diff-index
git --version
git show
git am
git reset
git ls-files
git diff-tree
git commit
git ls-remote
git checkout
git push
git add
git remote
git show-ref
主要用于解析git引用(如分支名称、标签名称等)或表达式,并输出对应的SHA-1值。它的作用包括但不限于: 解析提交、分支、标签等引用,获取对应的SHA-1值。 校验是否为有效的引用或表达式。 生成git对象的唯一标识符。 关联的lerna命令 lerna version:在执行版本升级操作时,lerna会使用git rev-parse来获取先前提交的哈希值作为上一个版本的参考。 lerna changed:用于列出自上次标记以来发生变更的包,可能会用到git rev-parse来比较不同提交之间的差异。 lerna diff:显示自上次标记以来的所有包的diff,也可能会使用git rev-parse来比较不同提交之间的差异。 lerna源码案例 libs/commands/import/src/index.ts
$ git rev-parse HEAD
f7f6d6f2b6b47eb8c4cf4b8bf5f83e0b8028c031
getCurrentSHA() {
return this.execSync("git", ["rev-parse", "HEAD"]);
}
getWorkspaceRoot() {
return this.execSync("git", ["rev-parse", "--show-toplevel"]);
}
主要用于根据最接近的标签来描述当前提交的位置。它的作用包括但不限于: 找到最接近当前提交的标签。 根据最接近的标签以及提交的SHA-1值生成一个描述字符串。 可以帮助识别当前提交相对于标签的距离,以及提交是否是基于标签进行的修改。 关联的lerna命令 lerna version:在执行版本升级操作时,lerna可能会使用git describe 来确定当前提交的位置,以便生成新的版本号。 lerna源码案例 libs/commands/diff/src/lib/get-last-commit.ts
$ git describe
polaris-release-1.0.0-c12345
export function getLastCommit(execOpts?: ExecOptions) {
if (hasTags(execOpts)) {
log.silly("getLastTagInBranch", "");
return childProcess.execSync("git", ["describe", "--tags", "--abbrev=0"], execOpts);
}
log.silly("getFirstCommit", "");
return childProcess.execSync("git", ["rev-list", "--max-parents=0", "HEAD"], execOpts);
}
主要用于列出提交对象的SHA-1哈希值。它的作用包括但不限于: 列出提交对象的哈希值,可以按时间、作者、提交者等顺序进行排序。 支持使用范围、分支、标签等参数来限制输出的提交范围。 关联的lerna命令 lerna changed:列出自上次标记以来发生变更的包,可能会使用git rev-list 来获取两个标记之间的提交列表。 lerna diff:显示自上次标记以来的所有包的diff,也可能会使用git rev-list 来获取两个标记之间的提交列表。 lerna 源码案例 libs/commands/diff/src/lib/get-last-commit.ts
$ git rev-list HEAD
f7f6d6f2b6b47eb8c4cf4b8bf5f83e0b8028c031
a3d8b4e1c2e1d0a9b8c6e5f7d6a4b3e8a1b2c3d4
export function getLastCommit(execOpts?: ExecOptions) {
if (hasTags(execOpts)) {
log.silly("getLastTagInBranch", "");
return childProcess.execSync("git", ["describe", "--tags", "--abbrev=0"], execOpts);
}
log.silly("getFirstCommit", "");
return childProcess.execSync("git", ["rev-list", "--max-parents=0", "HEAD"], execOpts);
}
主要用于比较索引和工作树之间的差异,并将其输出为标准输出。它的作用包括但不限于: 检查暂存区(index)和当前工作目录之间的差异。 可以与不同的选项一起使用,以便输出不同格式的差异信息。 关联的lerna命令 lerna changed:列出自上次标记以来发生变更的包时,可能会用到git diff-index来比较索引和工作树之间的差异。 lerna源码案例 libs/commands/import/src/index.ts
$ git diff-index HEAD
:100644 100644 bcd1234... 0123456... M file.txt
export class ImportCommand extends Command<ImportCommandOptions> {
...
if (this.execSync("git", ["diff-index", "HEAD"])) {
throw new ValidationError("ECHANGES", "Local repository has un-committed changes");
}
...
}
主要用于比较两棵树之间的差异,并以特定的格式输出。它的作用包括但不限于: 比较两个树对象之间的差异,例如提交对象和树对象之间的差异。 可以用于查看提交之间的差异,文件的更改等信息。 关联的lerna命令 lerna diff:显示自上次标记以来的所有包的diff时,可能会使用git diff-tree 来比较不同提交之间的差异。 lerna源码案例 libs/commands/publish/src/lib/get-projects-with-tagged-packages.ts
$ git diff-tree HEAD~2 HEAD
100644 blob a3d8b4e1c2e1d0a9b8c6e5f7d6a4b3e8a1b2c3d4 file.txt
export async function getProjectsWithTaggedPackages(
projectNodes: ProjectGraphProjectNodeWithPackage[],
projectFileMap: ProjectFileMap,
execOpts: ExecOptions
): Promise<ProjectGraphProjectNodeWithPackage[]> {
log.silly("getTaggedPackages", "");
// @see https://stackoverflow.com/a/424142/5707
// FIXME: --root is only necessary for tests :P
const result = await childProcess.exec(
"git",
["diff-tree", "--name-only", "--no-commit-id", "--root", "-r", "-c", "HEAD"],
execOpts
);
const stdout: string = result.stdout;
const files = new Set(stdout.split("\n"));
return projectNodes.filter((node) => projectFileMap[node.name]?.some((file) => files.has(file.file)));
}
主要用于显示引用(如分支和标签)的名称和其对应的提交哈希值。它的作用包括但不限于: 列出git仓库中的所有引用及其对应的提交哈希值。 可以用于查看分支、标签等引用的信息。 关联的lerna命令 lerna version:在执行版本升级操作时,lerna可能会使用git show-ref 来获取引用的信息,以确定当前提交的位置。 lerna源码案例 libs/commands/version/src/lib/remote-branch-exists.ts
$ git show-ref
a3d8b4e1c2e1d0a9b8c6e5f7d6a4b3e8a1b2c3d4 HEAD
a3d8b4e1c2e1d0a9b8c6e5f7d6a4b3e8a1b2c3d4 refs/heads/main
f7f6d6f2b6b47eb8c4cf4b8bf5f83e0b8028c031 refs/tags/v1.0.0
export function remoteBranchExists(gitRemote: string, branch: string, opts: ExecOptions) {
log.silly("remoteBranchExists", "");
const remoteBranch = `${gitRemote}/${branch}`;
try {
childProcess.execSync("git", ["show-ref", "--verify", `refs/remotes/${remoteBranch}`], opts);
return true;
} catch (e) {
return false;
}
}
四
总结
往期回顾
文 / 效率前端小丙
关注得物技术,每周一、三、五更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
未经得物技术许可严禁转载,否则依法追究法律责任。
“
扫码添加小助手微信
如有任何疑问,或想要了解更多技术资讯,请添加小助手微信: